﻿using System;
using System.Web;
using System.Collections;
using System.Configuration;
using System.IO;
using System.Collections.Generic;
using System.Net;
using Microsoft.SharePoint;
using System.Web.Caching;
using System.Text.RegularExpressions;

namespace RSPDS.MOSS.PrettyUrlController
{
    public class Controller : IHttpModule
    {
        #region Nested Classes

        public class Mapping
        {
            public string PrettyURL { get; set; }
            public string RealURL { get; set; }
            public bool Permanent { get; set; }

            public Mapping Clone()
            {
                return new Mapping
                {
                    PrettyURL = this.PrettyURL,
                    RealURL = this.RealURL,
                    Permanent = this.Permanent
                };
            }
        }

        #endregion Nested Classes

        #region Constants

        private const string IGNORE_EXTS = ".js;.css;.axd;.flv;.swf;.asmx";
        //private const string IGNORE_PATHS = "_vti_bin;_layouts;allitems.aspx;form.aspx";
        private const string IGNORE_PATHS = "lists/prettyurls;_layouts;WebResource.axd";
        private const string MAXAGE_PATHS = "/js/;/images/";

        #endregion Constants

        #region Properties

        public string BlobCacheValue
        {
            get
            {
                try
                {
                    // Get the max age of the blob cache.
                    var result = ConfigurationManager.GetSection("SharePoint/BlobCache") as Hashtable;
                    if (result != null) return result["max-age"].ToString();
                }
                catch (Exception)
                {
                    return string.Empty;
                }
                return string.Empty;
            }
        }

        public IList<String> IgnoreFileList { get; private set; }
        public IList<String> IgnorePathList { get; private set; }
        public IList<String> AddMaxAgePathList { get; private set; }

        public string IgnoreFilesString
        {
            get
            {
                try
                {
                    // Get igore file types from the configuration.
                    string result = ConfigurationManager.AppSettings["PrettyURLFileIgnoreList"];
                    if (String.IsNullOrEmpty(result))
                        result = IGNORE_EXTS;
                    return result;
                }
                catch (Exception)
                {
                    return IGNORE_EXTS;
                }
            }

        }

        public string IgnorePathsString
        {
            get
            {
                try
                {
                    string result = ConfigurationManager.AppSettings["PrettyURLPathIgnoreList"];
                    if (String.IsNullOrEmpty(result))
                        result = IGNORE_PATHS;
                    return result;
                }
                catch (Exception)
                {
                    return IGNORE_PATHS;
                }
            }
        }

        public string AddMaxAgePathString
        {
            get
            {
                try
                {
                    string result = ConfigurationManager.AppSettings["PrettyURLPathAddMaxAgeList"];
                    if (String.IsNullOrEmpty(result))
                        result = MAXAGE_PATHS;
                    return result;
                }
                catch (Exception)
                {
                    return MAXAGE_PATHS;
                }
            }
        }

        #endregion Properties

        #region Methods

        public Controller()
        {
            IgnoreFileList = String.IsNullOrEmpty(IgnoreFilesString)
                ? new List<String>() : new List<String>(IgnoreFilesString.Split(';'));
            IgnorePathList = String.IsNullOrEmpty(IgnorePathsString) ?
                new List<String>() : new List<String>(IgnorePathsString.Split(';'));
            AddMaxAgePathList = String.IsNullOrEmpty(AddMaxAgePathString) ?
                new List<String>() : new List<String>(AddMaxAgePathString.Split(';'));
        }

        public void Init(HttpApplication context)
        {
            context.PostAuthenticateRequest += OnEnter;
        }

        private void OnEnter(object sender, EventArgs e)
        {
            try
            {
                HttpApplication app = sender as HttpApplication;
                if (null == app) throw new SPException("Fatal error - HttpApplication is null");
                // get request path and query string of app
                string pathOnly = app.Request.Url.AbsolutePath.ToLower();
                string qs = app.Request.Url.Query;
                string extension = Path.GetExtension(pathOnly);
                if ((!string.IsNullOrEmpty(extension) && IgnoreFileList.ContainsCaseless(extension)) || IsIgnorePath(pathOnly))
                {
                    if ((extension == ".js" || extension == ".css") && pathOnly.StartsWith("/js/"))
                    {
                        string bvalue = BlobCacheValue;
                        if (!string.IsNullOrEmpty(bvalue))
                        {
                            TimeSpan ts = new TimeSpan(0, 0, 0, Int32.Parse(bvalue));
                            app.Response.Cache.SetMaxAge(ts);
                        }
                    }
                    return;
                }
                Mapping mapping;
                if (FoundMapping(pathOnly, out mapping))
                {
                    string realUrl = mapping.RealURL;
                    // Append additional query string value.
                    if (!String.IsNullOrEmpty(qs))
                        realUrl = (realUrl.Contains("?")) ? (realUrl + qs.Replace('?', '&')) : (realUrl + qs);
                    // Redirect with 301/302.
                    app.Response.Redirect(realUrl, false);
                    app.Response.StatusCode = (int)(mapping.Permanent ? HttpStatusCode.MovedPermanently : HttpStatusCode.TemporaryRedirect);
                    HttpContext.Current.ApplicationInstance.CompleteRequest();
                }

            }
            catch (Exception ex)
            {
                throw new SPException("Failed to resolve pretty URL", ex);
            }
        }

        private static bool FoundMappingRegEx(IDictionary<String, Mapping> mappings, string pathOnly, out Mapping mapping)
        {
            foreach (var pair in mappings)
            {
                var regex = new Regex(pair.Key, RegexOptions.IgnoreCase);
                if (regex.IsMatch(pathOnly))
                {
                    mapping = pair.Value.Clone();
                    if (null != mapping)
                        mapping.RealURL = regex.Replace(pathOnly, pair.Value.RealURL);
                    return true;
                }
            }
            // Never found it.
            mapping = null;
            return false;
        }

        private static Mapping FoundMapping(IDictionary<String, Mapping> mappings, string pathOnly)
        {
            string prettyUrl = pathOnly;
            Mapping result = null;
            if (mappings.ContainsKey(prettyUrl))
            {
                // First try an exact match.
                result = mappings[prettyUrl];
            }
            else if (!FoundMappingRegEx(mappings, pathOnly, out result))
            {
                // Match failed. Check to see if we need to strip or add a trailing slash.
                if (!pathOnly.EndsWith("/"))
                    prettyUrl = pathOnly + "/";
                else
                    prettyUrl = pathOnly.Substring(0, pathOnly.Length - 1);
                if (mappings.ContainsKey(prettyUrl))
                {
                    result = mappings[prettyUrl];
                }
                else if (!FoundMappingRegEx(mappings, pathOnly, out result))
                {
                    result = null;
                }
            }
            return result;
        }

        private static bool FoundMapping(string pathOnly, out Mapping mapping)
        {
            bool result = false;
            mapping = null;
            var mappings = GetMappings();
            if (null != mappings)
            {
                mapping = FoundMapping(mappings, pathOnly);
                // If we found a match, see if we also found an exclusion.
                if (null != mapping)
                {
                    mappings = GetMappings(true);
                    // If no exclusion match then proceed with the mapping.
                    if (null == FoundMapping(mappings, pathOnly))
                        result = true;
                }
            }
            return result;
        }

        private static void FlushCache(bool exclude = false)
        {
            ProcessPrettyUrlList(HttpContext.Current.Request.Url.ToString(), delegate(SPWeb web, SPList prettyURLList)
            {
                var cacheKey = String.Format("RSPDS_PrettyUrl_Mappings_{0}_{1}", web.Site.ID, exclude);
                Cache cache = HttpContext.Current.Cache;
                if (null != cache && null != cache[cacheKey])
                    cache.Remove(cacheKey);
            });
        }

        private static T GetFieldValue<T>(SPListItem item, string fieldName)
        {
            try
            {
                return (T)item[fieldName];
            }
            catch (Exception)
            {
                return default(T);
            }
        }

        private static bool ProcessPrettyUrlList(string url, Action<SPWeb, SPList> del)
        {
            if (null == del) throw new ArgumentNullException("del");
            if (String.IsNullOrEmpty(url)) throw new ArgumentNullException("url");
            try
            {
                using (var currSite = new SPSite(url))
                {
                    SPFeature prettyURLFeature = currSite.Features[new Guid("054b8082-0ff4-416d-a28e-003eedea7fa4")];
                    if (null == prettyURLFeature) return false;
                    // Check the root web for the pretty URLs list
                    SPWeb rootWeb = currSite.RootWeb;
                    SPList prettyURLList = rootWeb.Lists["Pretty URLs"];
                    if (null == prettyURLList) return false;
                    del(currSite.RootWeb, prettyURLList);
                    return true;
                }
            }
            catch (Exception)
            {
                return false;
            }
        }

        private static IDictionary<String, Mapping> GetMappings(bool exclude = false)
        {
            try
            {
                var result = new Dictionary<String, Mapping>(StringComparer.OrdinalIgnoreCase);
                ProcessPrettyUrlList(HttpContext.Current.Request.Url.ToString(), delegate(SPWeb web, SPList prettyURLList)
                {
                    Cache cache = HttpContext.Current.Cache;
                    string cacheKey = String.Format("RSPDS_PrettyUrl_Mappings_{0}_{1}", web.Site.ID, exclude);
                    // Check to see if we've been asked to flush the cache.
                    HttpRequest request = HttpContext.Current.Request;
                    string flushCacheString = request["flushcache"];
                    bool flushCache = false;
                    if (!String.IsNullOrEmpty(flushCacheString))
                    {
                        Boolean.TryParse(flushCacheString, out flushCache);
                        // Have we been asked to flush the cache and the item is in the cache?
                        if (flushCache)
                        {
                            // Flush both include and exclude cache.
                            FlushCache(false);
                            FlushCache(true);
                        }
                    }
                    // Check the cache.
                    if (!flushCache && null != cache && null != cache[cacheKey])
                    {
                        // Item is in the cache and we didn't flush.
                        result = cache[cacheKey] as Dictionary<String, Mapping>;
                    }
                    else
                    {
                        //create new spquery to get splistitemcollection (best practice)
                        SPQuery listQuery = new SPQuery
                        {
                            Query = "<Where><Eq><FieldRef Name='Exclude' /><Value Type='Boolean'>" +
                                (exclude ? "1" : "0") + "</Value></Eq></Where>",
                            ViewFields = "<FieldRef Name='Pretty URL'/><FieldRef Name='Real URL'/><FieldRef Name='Permanent'/>"
                        };
                        SPListItemCollection listItems = prettyURLList.GetItems(listQuery);
                        // Iterate the list items in the splistitemcollection (best practice)
                        foreach (SPListItem listItem in listItems)
                        {
                            string prettyUrl = GetFieldValue<String>(listItem, "Pretty URL");
                            string realUrl = GetFieldValue<String>(listItem, "Real URL");
                            bool permanent = GetFieldValue<Boolean>(listItem, "Permanent");
                            if (!String.IsNullOrEmpty(prettyUrl))
                            {
                                string key = prettyUrl.Trim();
                                var mapping = new Mapping
                                {
                                    PrettyURL = key,
                                    RealURL = null == realUrl ? String.Empty : realUrl.Trim(),
                                    Permanent = permanent
                                };
                                if (!result.ContainsKey(key))
                                    result.Add(key, mapping);
                                else
                                    result[key] = mapping;
                            }
                        }
                        // Add to cache.
                        if (null != cache)
                            cache.Add(cacheKey, result, null, DateTime.Now.AddHours(8), Cache.NoSlidingExpiration, CacheItemPriority.Normal, null);
                    }
                });
                return result;
            }
            catch (ArgumentNullException)
            {
                return null;
            }
        }

        private bool IsIgnorePath(string pathOnly)
        {
            return IgnorePathList.ContainsCaseless(pathOnly);
        }

        public void Dispose()
        {

        }

        #endregion Methods
    }
}